package jmine.tec.utils.xml;

import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.w3c.dom.Element;

/**
 * XML Utils methods.
 * 
 * @author takeshi
 */
public class XMLUtils {

    private static final String TIME_ZONE_PATTERN_STRING = "([-+]\\d{2}:\\d{2})?";

    private static final String DATE_ONLY_PATTERN = "(-?\\d{4,})-(\\d{2})-(\\d{2})";

    public static final String TIME_PATTERN_STRING = "(\\d{2}):(\\d{2}):(\\d{2}(?:[.]\\d+)?)";

    public static final String DATE_TIME_PATTERN =
            XMLUtils.DATE_ONLY_PATTERN + "T" + XMLUtils.TIME_PATTERN_STRING + XMLUtils.TIME_ZONE_PATTERN_STRING;

    public static final String DATE_PATTERN = XMLUtils.DATE_ONLY_PATTERN + XMLUtils.TIME_ZONE_PATTERN_STRING;

    private static final int MILLIS = 1000;

    private static final int TIME_ZONE_INDEX = 7;

    private static final int HOUR_INDEX = 4;

    private static final int DAY_INDEX = 3;

    /**
     * Holder class for lazy initialization.
     * 
     * @author takeshi
     */
    private static final class DatePatternHolder {
        /**
         */
        private DatePatternHolder() {
        }

        public static final Pattern DATE_PATTERN = Pattern.compile(XMLUtils.DATE_PATTERN);
    }

    /**
     * Holder class for lazy initialization.
     * 
     * @author takeshi
     */
    private static final class TimePatternHolder {
        /**
         */
        private TimePatternHolder() {
        }

        public static final Pattern TIME_PATTERN = Pattern.compile(XMLUtils.TIME_PATTERN_STRING);
    }

    /**
     * Holder class for lazy initialization.
     * 
     * @author takeshi
     */

    private static final class DateTimePatternHolder {
        /**
         */
        private DateTimePatternHolder() {
        }

        public static final Pattern DATE_TIME_PATTERN = Pattern.compile(XMLUtils.DATE_TIME_PATTERN);
    }

    /**
     * @param source {@link String}
     * @return {@link Date}
     */
    public Date parseDateTime(final String source) {
        Calendar cal = this.parseDateTimeAsCalendar(source);
        if (cal != null) {
            return cal.getTime();
        }
        return null;
    }

    /**
     * Parse
     * 
     * @param source {@link String}
     * @return {@link Date}
     */
    public Date parseDate(final String source) {
        Calendar cal = this.parseDateAsCalendar(source);
        return cal != null ? cal.getTime() : null;
    }

    /**
     * @param source {@link String}
     * @return {@link Calendar} or <code>null</code>
     */
    public Calendar parseTimeAsCalendar(final String source) {
        if (source == null) {
            return null;
        }
        Matcher matcher = TimePatternHolder.TIME_PATTERN.matcher(source);
        if (matcher.matches()) {
            return this.extractTime(matcher);
        }
        return null;
    }

    /**
     * @param matcher the {@link Matcher}
     * @return {@link Calendar}
     */
    private Calendar extractTime(final Matcher matcher) {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        this.extractTime(cal, matcher, 1);
        return cal;
    }

    /**
     * @param source the source
     * @return {@link Date}
     */
    public Date parseTime(final String source) {
        Calendar cal = this.parseTimeAsCalendar(source);
        return cal != null ? cal.getTime() : null;
    }

    /**
     * @param source {@link String}
     * @return {@link Calendar}
     */
    public Calendar parseDateAsCalendar(final String source) {
        if (source == null) {
            return null;
        }
        Matcher matcher = DatePatternHolder.DATE_PATTERN.matcher(source);
        if (matcher.matches()) {
            return this.extractDate(matcher);
        }
        return null;
    }

    /**
     * @param matcher {@link Matcher}
     * @return {@link Calendar}
     */
    private Calendar extractDate(final Matcher matcher) {
        final int f = 4;
        Calendar cal = this.createCalendar(matcher, f);
        this.extractDate(cal, matcher);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal;
    }

    /**
     * Parses the datetime string as a {@link Calendar}
     * 
     * @param source the {@link String}
     * @return {@link Calendar}
     */
    public Calendar parseDateTimeAsCalendar(final String source) {
        if (source == null) {
            return null;
        }
        Calendar cal = null;
        Matcher matcher = DateTimePatternHolder.DATE_TIME_PATTERN.matcher(source.trim());
        if (matcher.matches()) {
            cal = this.extractDateTime(matcher);
        }
        return cal;
    }

    /**
     * @param matcher {@link Matcher}
     * @return {@link Calendar}
     */
    private Calendar extractDateTime(final Matcher matcher) {
        Calendar cal;
        final int timeZoneIndex = TIME_ZONE_INDEX;
        cal = this.createCalendar(matcher, timeZoneIndex);
        this.extractDate(cal, matcher);
        final int offset = HOUR_INDEX;
        this.extractTime(cal, matcher, offset);
        return cal;
    }

    /**
     * @param matcher the {@link Matcher}
     * @param timeZoneIndex the timezone index in the matcher
     * @return {@link Calendar}
     */
    private Calendar createCalendar(final Matcher matcher, final int timeZoneIndex) {
        Calendar cal;
        TimeZone tz = TimeZone.getDefault();
        final String group = matcher.group(timeZoneIndex);
        if (group != null) {
            tz = this.extractTimeZone(group);
        }
        cal = Calendar.getInstance(tz);
        return cal;
    }

    /**
     * @param cal {@link Calendar}
     * @param matcher {@link Matcher}
     */
    private void extractDate(final Calendar cal, final Matcher matcher) {
        int y = Integer.parseInt(matcher.group(1));
        int m = Integer.parseInt(matcher.group(2));
        int d = Integer.parseInt(matcher.group(DAY_INDEX));
        cal.set(Calendar.YEAR, y);
        cal.set(Calendar.MONTH, m - 1);
        cal.set(Calendar.DAY_OF_MONTH, d);
    }

    /**
     * @param cal {@link Calendar}
     * @param matcher {@link Matcher}
     * @param offset matcher offset
     */
    private void extractTime(final Calendar cal, final Matcher matcher, final int offset) {
        int h = Integer.parseInt(matcher.group(offset));
        int min = Integer.parseInt(matcher.group(offset + 1));
        double sec = Double.parseDouble(matcher.group(offset + 2));
        cal.set(Calendar.HOUR_OF_DAY, h);
        cal.set(Calendar.MINUTE, min);
        cal.set(Calendar.SECOND, (int) sec);
        sec = sec - Math.floor(sec); // extracting millis
        int millis = (int) (sec * MILLIS);
        cal.set(Calendar.MILLISECOND, millis);
    }

    /**
     * Extracts the time zone as {@link TimeZone#getTimeZone(String)} expects (ex. GMT-0300)
     * 
     * @param zone the raw timezone from the xml file
     * @return {@link TimeZone}
     */
    private TimeZone extractTimeZone(final String zone) {
        TimeZone tz;
        String s = "GMT" + zone.replaceAll(":", "");
        tz = TimeZone.getTimeZone(s);
        return tz;
    }

    /**
     * Returns the given attribute as a Date or <code>null</code> if none available or the attribute doesnt match.
     * 
     * @param element {@link Element}
     * @param attributeName {@link String}
     * @return {@link Date}
     */
    public Date getAttributeAsDate(final Element element, final String attributeName) {
        return this.parseDate(element.getAttribute(attributeName));
    }

    /**
     * Returns the given attribute as a Date or the default value if the attribute is not available or does not match.
     * 
     * @param element {@link Element}
     * @param attributeName {@link String}
     * @param defaultValue {@link Date}
     * @return {@link Date}
     */
    public Date getAttributeAsDate(final Element element, final String attributeName, final Date defaultValue) {
        Date d = this.parseDate(element.getAttribute(attributeName));
        return d == null ? defaultValue : d;
    }

    /**
     * Gets an attribute as a double or return the given default value if the attribute is not found or does not match.
     * 
     * @param element {@link Element}
     * @param attributeName {@link String}
     * @param defaultValue double
     * @return double
     */
    public double getAttributeAsDouble(final Element element, final String attributeName, final double defaultValue) {
        String val = element.getAttribute(attributeName);
        try {
            return val == null ? defaultValue : Double.parseDouble(val);
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    /**
     * Gets the attribute as a boolean, returning the given default value if none found or if the attribute does not match either 'true' or
     * 'false'.
     * 
     * @param element {@link Element}
     * @param name the attribute name
     * @param defaultValue the default value
     * @return boolean
     */
    public boolean getAttributeAsBoolean(final Element element, final String name, final boolean defaultValue) {
        String val = element.getAttribute(name);
        if (val == null) {
            return defaultValue;
        }
        if ("true".equals(val)) {
            return true;
        } else if ("false".equals(val)) {
            return false;
        }

        return defaultValue;
    }

    /**
     * Parses a field as a {@link Calendar} if possible or returns <code>null</code>.
     * 
     * @param source {@link String}
     * @return {@link Calendar}
     */
    public Calendar parseAsDateOrTime(final String source) {
        if (source == null) {
            return null;
        }
        Matcher matcher = DateTimePatternHolder.DATE_TIME_PATTERN.matcher(source);
        if (matcher.matches()) {
            return this.extractDateTime(matcher);
        }
        matcher = TimePatternHolder.TIME_PATTERN.matcher(source);
        if (matcher.matches()) {
            return this.extractTime(matcher);
        }
        matcher = DatePatternHolder.DATE_PATTERN.matcher(source);
        if (matcher.matches()) {
            return this.extractDate(matcher);
        }
        return null;
    }

}
